/* 
 * Quechua - the lightweight data mining framework
 *
 * Copyright (C) 2012 Marek Denis <quechua@octogan.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <libconfig.h++>

#include "../include/modules.h"
#include "../include/quechua.h"

Module::Module(const string_t& name,const string_t& path,
                   g_module* module) {
    this->name = name;
    this->path = path;
    this->module=module;
}

Module::~Module() {
    LOG(INFO) << "Unloading module " << name << " from " << path;
    delete module;
}

const string_t& Module::get_name() const {
    return name;
}

const string_t& Module::get_path() const {
    return path;
}

g_module* Module::get_module() const {
    return module;
}

bool Module::get_symbol(const string_t& symbol_name, void*& symbol)
{
    return module->get_symbol(symbol_name,symbol);
}
bool Modules::load_modules(const configuration_t& config) {
    int length;
    if(!config.exists("quechua.libraries")) {
        LOG(ERROR) << "quechua.libraries entry not found in the configuration file";
        return false;
    }

    setting_t& libraries = config.lookup("quechua.libraries");
    
    if(!libraries.exists("load")) {
        LOG(ERROR) << "quechua.libraries.load entry not found in the configuration file";
        return false;
    }

    length = libraries["load"].getLength();
    for(int i=0;i<length;++i) {
        string_t libfile;
        Module *module = NULL;
        try {
            const string_t libname = libraries["load"][i];
            bool loaded = libraries["files"].lookupValue(libname, libfile);
            if(!loaded){
                LOG(WARN) << "Module name " << libname
                          << " not presented in 'files' dictionary in the configuration "
                          << " skipping...";
                continue;
            }
            if(module_loaded(libname)){
                LOG(WARN) << "Module " << libname 
                          << " already loaded, skipping";
                continue;
            }
            
            string_t path = path_prefix + libfile;

            LOG(INFO) << "Loading module " << path << " ...";
            g_module* shared_module = NULL;
            shared_module = new g_module(path);
            
            if(!shared_module || !(*(shared_module))) {
                LOG(ERROR) << "Error loading module " << libname
                           << " from " << path << ". "
                           << shared_module->get_last_error();
                continue;
            }

            module = new Module(libname,path,shared_module);
                
            if(!modules.insert(std::pair<string_t,Module*>(module->get_name(),module)).second) {
                LOG(ERROR) << "Error when inserting module " << module->get_name();
                delete module;
                continue;
            }
            
        } catch(setting_ex& ex) {
            LOG(ERROR) << "Setting not found";
            if(module)
                delete module;
            continue;
        } catch(parse_ex& ex) {
            LOG(ERROR) << "Error while parsing configuration file:" << ex.what();
            if(module)
                delete module;
            continue;
        }
    }
    return true;
}
bool Modules::load_components(const configuration_t& config) {
    if(!config.exists("quechua.channels")) {
        LOG(WARN) << "No channels configured, how do you want to get your input?";
    }
    else {
        setting_t& channels = config.lookup("quechua.channels");
        load_channels(channels);
    }

    if(!config.exists("quechua.loggers")) {
        LOG(WARN) << "No loggers configured, how do you want to log your data?";
    }
    else {
        setting_t& loggers = config.lookup("quechua.loggers");
        load_loggers(loggers);
    }
    
    if(!config.exists("quechua.flows")) {
        LOG(WARN) << "No flows configures.";
    }
    else {
        setting_t& flows = config.lookup("quechua.flows");
        LOG(INFO) << "Loading workflows";
        load_workflows(flows);
    }
};

bool Modules::load_channels(const setting_t& channel_settings) {
    
    load_channel_ptr create_channel = NULL;
    if(!channel_settings.exists("load")) {
        LOG(WARN) << "quechua.channels.load entry not found";
        return false;
    }
    int length = channel_settings["load"].getLength();

    for(int i=0;i<length;++i) {
        string_t channel_name = channel_settings["load"][i];
        LOG(INFO) << "Loading channel " << channel_name;

        Module* module = load_module_s(channel_name);

        if(!module) {
            LOG(ERROR) << "Error while loading module " << channel_name
                       << " skipping";
            continue;
        }

        if(!(module->get_symbol("create_channel",(void*&)create_channel))) {
            LOG(ERROR) << "Cannot find function create_channel in module "
                       << module->get_path() << ", skipping";
            continue;
        }
        
        if(create_channel==NULL) {
            LOG(ERROR) << "load_channel pointer is NULL, skipping!";
            continue;
        }
        
        Channel* channel = NULL;  
        channel = create_channel();
        if(channel == NULL) {
            LOG(ERROR) << "Cannot create channel " << channel_name << " from module "
                       << module->get_path();
            continue;
        }
        
        channel->internal->name = channel_name;
        
        try {
            channel->load_misc_conf(channel_settings[channel_name]);
        } catch(setting_ex& ex) {
            LOG(WARN) << "Channel " << channel_name 
                      << ": Some settings were not found, check your config and/or channel implementation.";
        } catch(parse_ex& ex) {
            LOG(WARN) << "Channel " << channel_name 
                      << ": Options parsing error, check your config and/or channel implementation.";
        }


        if( (g_quechua->components->channels.insert(std::pair<string_t,Channel*>(channel_name,channel))).second) {
            LOG(INFO) << "Channel " << channel_name <<" loaded sucessfuly!";
        } else {
            LOG(WARN) << "Channel named " << channel_name << " already loaded";
            delete channel;
        }
    }
    return true;
};

bool Modules::load_loggers(const setting_t& logger_settings) {
    load_logger_ptr create_logger = NULL;
    if(!logger_settings.exists("load")) {
        LOG(WARN) << "quechua.loggers.load entry not found";
        return false;
    }
    int length = logger_settings["load"].getLength();
    for(int i=0;i<length;++i) {
        const string_t logger_name = logger_settings["load"][i];
        LOG(INFO) << "Loading logger " << logger_name << " ";

        Module* module = load_module_s(logger_name);

        if(!module) {
            LOG(ERROR) << "Error while loading module " << logger_name
                       << " skipping";
            continue;
        }
        if(!(module->get_symbol("create_logger",(void*&)create_logger))) {
            LOG(ERROR) << "Cannot find function create_logger in module "
                       << module->get_path() << " skipping";
            continue;
        }
        
        if(create_logger==NULL) {
            LOG(ERROR) << " Symbol create_logger() wasn't found!";
            continue;
        }
        
        Logger* logger = NULL; 

        logger = create_logger();
        if(logger == NULL) {
            LOG(ERROR) << "Cannot create logger " << logger_name << " from module "
                       << module->get_path();
            continue;
        }
        
        logger->internal->name = logger_name;
        try {
            logger->load_misc_conf(logger_settings[logger_name]);
        } catch(setting_ex& ex) {
            LOG(WARN) << "Logger " << logger_name 
                      << ": Some settings were not found, check your config and/or channel implementation.";
        } catch(parse_ex& ex) {
            LOG(WARN) << "Logger " << logger_name 
                      << ": Options parsing error, check your config and/or channel implementation.";
        }
        
        if( (g_quechua->components->loggers.insert(std::pair<string_t,Logger*>(logger_name,logger))).second) {
            LOG(INFO) << "Logger " << logger_name << " loaded sucessfuly!";
        } else {
             LOG(WARN) << "Logger named" << logger_name << " already inserted";
            delete logger;
       }
    }
    return true;
};

/**
 * Here's how it works:
 * 1) Create Workflow object
 * 2) Read the channel name from config and register 
 * 3) Read the logger name from the config and register 
 * 4) Create processor and algorithm object 
 * 5) store itself in the g_quechua workflows list 
 */
bool Modules::load_workflows(const setting_t& flow_settings) {
    if(!flow_settings.exists("load")) {
        LOG(WARN) << "quechua.flows.load not found, no workflows created";
        return false;
    }
    
    ushort length = flow_settings["load"].getLength();
    for(ushort i=0;i<length;++i) {
        const string_t flow_name = flow_settings["load"][i];
        
        if(!flow_settings.exists(flow_name)) {
            LOG(WARN) << "Workflow " << flow_name
                      << " not found in the config file, skipping.";
            continue;
        }
        
        Workflow* flow = new Workflow(flow_name,g_quechua->loop);
        try {
            flow->init_internals(flow_settings[flow_name]);
        } catch(const setting_ex& ex) {
            LOG(ERROR) << "Cannot find " << flow_name << " section in configuration file";
            delete flow;
//            return false;
            continue;
        };

        Channel* channel = NULL;
        Logger*  logger  = NULL;

        if( (channel = register_flow_in_channels(flow_settings[flow_name],flow,&(g_quechua->components->channels)))!=NULL) {
            flow->add_channel(channel);
        } else {
            LOG(WARN) << "Workflow " << flow_name << ": Couldn't find appropriate channel, skipping...";
        }
    
        if((logger = register_flow_in_loggers(flow_settings[flow_name],flow,&(g_quechua->components->loggers)))!=NULL) {
            flow->add_logger(logger);
        } else {
            LOG(WARN) << "Workflow " << flow_name << ": Couldn't find appripriate logger, skipping...";
        }

        
        if(!g_quechua->components->workflows.insert(
                std::pair<string_t,workflow_t>(flow_name,flow)).second) {
            LOG(ERROR) << "Workflow " << flow_name << " was not inserted into Quechua database";
            unregister_flow_from_channels(flow,channel);
  //          unregister_flow_from_loggers(flow,logger);
            delete flow;
            continue;
        }

    }
    return true;
}

Algorithm* Modules::load_algorithm(const string_t& algorithm_name,
                                   const setting_t& algorithm_settings) {
    Module* module = load_module_s(algorithm_name);

    if(!module) {
        LOG(ERROR) << "Error while loading algorithm " << algorithm_name
                   << " skipping";
        return NULL;
    }

    load_algorithm_ptr create_algorithm = NULL;

    if(!(module->get_symbol("create_algorithm",(void*&)create_algorithm))) {
        LOG(ERROR) << "Symbol create_algorithm() was not found in module "
                   << module->get_path() << " skipping";
        return NULL;
    }
    
    if(create_algorithm==NULL) {
        LOG(ERROR) << "Symbol create_algorithm() is set to NULL";
        return NULL;
    }
    
    Algorithm* algorithm = NULL;  
    algorithm = create_algorithm();

    if(algorithm == NULL) {
        LOG(ERROR) << "Cannot create algorithm " << algorithm_name << " from module "
                   << module->get_path();
        return NULL;
    }
    algorithm->internal->name = algorithm_name;

    try {
        algorithm->load_misc_conf(algorithm_settings);
    } catch(setting_ex& ex) {
        LOG(WARN) << "Algorithm " << algorithm_name 
                  << ": Some settings were not found, check your config and/or algorithm implementation.";
    } catch(parse_ex& ex) {
        LOG(WARN) << "Algorithm " << algorithm_name
                  << ": Options parsing error, check your config and/or algorithm implementation.";
    }

    return algorithm;
};

Processor* Modules::load_processor(const string_t& processor_name,
                                   const setting_t& processor_settings) {

    Module* module = load_module_s(processor_name);

    if(!module) {
        LOG(ERROR) << "Error while loading module " << processor_name
                   << " skipping";
        return NULL;
    }

    load_processor_ptr create_processor = NULL;

    if(!(module->get_symbol("create_processor",(void*&)create_processor))) {
        LOG(ERROR) << "Cannot find function create_processor in module "
                   << module->get_path() << " skipping";
        return NULL;
    }
    
    if(create_processor==NULL) {
        LOG(ERROR) << "Symbol create_processor IS STILL NULL!";
        return NULL;
    }
    
    Processor* processor = NULL;  
    processor = create_processor();

    if(processor == NULL) {
        LOG(ERROR) << "Cannot create processor " << processor_name << " from module "
                   << module->get_path();
        return NULL;

    }


    processor->internal->name = processor_name;
    
    try {
        processor->load_misc_conf(processor_settings);
    } catch(setting_ex& ex) {
        LOG(WARN) << "Processor " << processor_name 
                  << ": Some settings were not found, check your config and/or processor implementation.";
    } catch(parse_ex& ex) {
        LOG(WARN) << "Processor " << processor_name
                  << ": Options parsing error, check your config and/or processor implementation.";
    }

    return processor;
};

size_t Modules::size() {
    return modules.size();
}

Module* Modules::load_module_s(const string_t& kName) {
    Module* module = NULL;
    
    map_module_it module_it;
    if( (module_it=modules.find(kName)) == modules.end() ){
        LOG(ERROR) << "Module " << kName << " not loaded";
        return NULL;
    }

    module = module_it->second;
    
    return module;
}

bool Modules::module_loaded(const string_t& modname) {
    if(modules.find(modname)!=modules.end())
        return true;
    return false;
}

Channel* const register_flow_in_channels(const setting_t& flow_settings,
                               Workflow* flow,
                               channels_map* channels) {
    string_t channel_name;
    flow_settings.lookupValue("channel",channel_name);
    channels_map_it map_it = channels->find(channel_name);
    if(map_it==channels->end()) {
        LOG(WARN) << "Channel " << channel_name <<  "was not loaded";
        return NULL;
    }
    Channel* channel = map_it->second;
    channel->record(flow);
    return channel;
}

void unregister_flow_from_channels(Workflow* flow, Channel* channel) {
  channel->unrecord(flow); 
}


/**
 * TODO(marek): Change the name to be more accurate
 **/ 

Logger* const register_flow_in_loggers(const setting_t& flow_settings,Workflow* flow, loggers_map* loggers) {
    string_t logger_name;
    flow_settings.lookupValue("logger",logger_name);
    loggers_map_it map_it = loggers->find(logger_name);
    if(map_it==loggers->end()) {
        LOG(WARN) << "Logger " << logger_name << " was not loaded";
        return NULL;
    }
    Logger* logger = map_it->second;
    return logger; 
}
void unregister_flow_from_loggers(Workflow* flow, Logger* logger) {

    LOG(WARN) << "ATTENTION: In this version function unregister_flow_from_loggers() does nothing!"
              << " Do not rely on it!";
//    logger->unrecord(flow);
}
